package cn.coder.toolkit;

import lombok.Getter;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Base64;

/**
 * AES 加密工具
 * <p>
 * 密钥加密/密钥加密
 * 密钥转换/密钥生成
 * <p>
 * 业内规范: 字符串格式的 密钥/密文 等默认都使用 BASE64 编码
 * <p>
 * Java 中 AES 加密默认使用 AES/ECB/PKCS5Padding, 即 Cipher.getInstance("AES") 等效于 Cipher.getInstance("AES/ECB/PKCS5Padding"), 加解密双方必须使用相同的加密模式和填充方式还有初始向量IV
 * <p>
 * AES密钥长度128/192/256，其中192与256需要配置无政策限制权限文件
 * <p>
 * 要理解AES的加密流程，会涉及到AES加密的五个关键词，分别是：分组密码体制、四种加密模式(不确定是否有更多)、填充方式、密钥、初始向量IV
 * 分组密码体制：所谓分组密码体制就是指将明文切成一段一段的来加密，然后再把一段一段的密文拼起来形成最终密文的加密方式。
 * 				AES采用分组密码体制，即AES加密会首先把明文切成一段一段的，而且每段数据的长度要求必须是128位16个字节，（BlockSize，128/192/256都是128位16字节）
 * 				如果最后一段不够16个字节了，就需要用Padding来把这段数据填满16个字节，然后分别对每段数据进行加密，最后再把每段加密数据拼起来形成最终的密文。
 * 加密模式：AES一共有四种加密模式，
 * 				ECB（电子密码本模式）
 * 				CBC（密码分组链接模式）
 * 				CFB
 * 				OFB
 * 				我们一般使用的是CBC模式。四种模式中除了ECB相对不安全之外，其它三种模式的区别并没有那么大
 * 填充方式：填充方式就是用来把不满16个字节的分组数据填满16个字节用的，它有三种模式PKCS5、PKCS7和NOPADDING。
 * 				PKCS5是指分组数据缺少几个字节，就在数据的末尾填充几个字节的几，比如缺少5个字节，就在末尾填充5个字节的5。
 * 				PKCS7是指分组数据缺少几个字节，就在数据的末尾填充几个字节的0，比如缺少7个字节，就在末尾填充7个字节的0。
 * 				NoPadding是指不需要填充，也就是说数据的发送方肯定会保证最后一段数据也正好是16个字节。
 * 				那如果在PKCS5模式下，最后一段数据的内容刚好就是16个16怎么办？那解密端就不知道这一段数据到底是有效数据还是填充数据了，因此对于这种情况，
 * 					PKCS5模式会自动帮我们在最后一段数据后再添加16个字节的数据，而且填充数据也是16个16，这样解密段就能知道谁是有效数据谁是填充数据了。
 * 					PKCS7最后一段数据的内容是16个0，也是同样的道理。解密端需要使用和加密端同样的Padding模式，才能准确的识别有效数据和填充数据。我们开发通常采用PKCS7 Padding模式。(好像Java默认是PKCS#5Padding)
 * 初始向量IV(CBC加密模式下才使用的到)：初始向量IV的作用是使加密更加安全可靠，我们使用AES加密时需要主动提供初始向量，而且只需要提供一个初始向量就够了，后面每段数据的加密向量都是前面一段的密文。
 * 				初始向量IV的长度规定为128位16个字节，初始向量的来源为随机生成。
 * 密钥：AES要求密钥的长度可以是128位16个字节、192位或者256位，位数越高，加密强度自然越大，但是加密的效率自然会低一些，因此要做好衡量。
 * 				我们开发通常采用128位16个字节的密钥，我们使用AES加密时需要主动提供密钥，而且只需要提供一个密钥就够了，每段数据加密使用的都是这一个密钥，密钥来源为随机生成。
 *
 * <p>
 * opmode(操作模式)是必须参数，可选值是ENCRYPT_MODE、DECRYPT_MODE、WRAP_MODE和UNWRAP_MODE。
 * Key类型参数如果不是非对称加密，对应的类型是SecretKey，如果是非对称加密，可以是PublicKey或者PrivateKey。
 * SecureRandom(Cipher.init)是随机源，因为有些算法需要每次加密结果都不相同，这个时候需要依赖系统或者传入的随机源，一些要求每次加解密结果相同的算法不能使用此参数。
 * <p>
 * Java Cryptography Architecture
 * Standard Algorithm Name Documentation for JDK 8
 * Java密码体系结构 JDK 8的标准算法名称文档
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyFactory
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#algspec
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Signature
 * 参考
 * AES加解密算法的模式介绍
 * https://blog.csdn.net/searchsun/article/details/2516191
 * JDK安全模块JCE核心Cipher使用详解
 * https://blog.csdn.net/zcmain/article/details/90640797
 * SecureRandom是随机源，因为有些算法需要每次加密结果都不相同，这个时候需要依赖系统或者传入的随机源，一些要求每次加解密结果相同的算法如AES不能使用此参数
 * 如何生成一个安全的密钥
 * https://www.baeldung.com/java-secure-aes-key
 * Java AES Encryption and Decryption
 * https://www.baeldung.com/java-aes-encryption-decryption
 */
public final class AESKit {

	public static void main(String[] args) {
		String data = "aa";
		String aesKeyStr = toKeyStr(generateKeyFromSpecialKeyStr("你好啊美女0"));
		System.out.println(aesKeyStr);
		System.out.println(new String(toKey(aesKeyStr).getEncoded()));
		String encryptedDataStr = encryptToStrByKeyStrAndDefaultIv(aesKeyStr, data);
		System.out.println(encryptedDataStr);
		System.out.println(decryptToStrByKeyStrAndDefaultIv(aesKeyStr, encryptedDataStr));
	}

	private AESKit() {}

	/**
	 * UTF-8编码
	 */
	private static final Charset UTF8 = StandardCharsets.UTF_8;
	/**
	 * 加密算法
	 */
	private static final String ALGORITHM_AES = "AES";
	private static final String ALGORITHM_PBKDF2WithHmacSHA256 = "PBKDF2WithHmacSHA256";
	/**
	 * 密钥长度(位),AES密钥长度可以为128/192/256，其中192和256需要配置无政策限制权限文件(JDK1.8.x改成无限制了)
	 */
	@Getter
	public enum KeySizeEnum {
		BIT_128(128),
		BIT_192(192),
		BIT_256(256);
		KeySizeEnum(int length) {
			this.length = length;
		}
		private final int length;
	}
	/**
	 * 默认TRANSFORMATION(转换模式)
	 */
	private static final String TRANSFORMATION = "AES/CBC/PKCS5PADDING";
	/**
	 * 默认CBC模式使用的初始向量(不可更改,否则可能导致加解密使用了不同的向量)
	 * UUID.randomUUID().toString().replace("-", "").substring(8, 24)
	 */
	private static final AlgorithmParameterSpec DEFAULT_IV = generateIvFromSpecialKeyStr("685a446fb5ea77c2");

	// 以下是生成密钥相关方法 ---------- ---------- ---------- ---------- ----------

	/**
	 * 生成密钥 (将指定字符串转换为密钥, 通常用于与其他语言交互, 需要向对方提供我方加解密使用的IV和KEY)
	 * @param key 为128/192/256位的字符串, 即占用16/24/32字节的字符串, 否则会在加密的时候报 java.security.InvalidKeyException: Invalid AES key length: 17 bytes
	 *
	 * 前端加解密流程如下
	 * <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
	 * <script src="https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.3.2/jsencrypt.js"></script>
	 * <script src="https://cdn.jsdelivr.net/npm/encryptlong@3.1.4/bin/jsencrypt.min.js"></script>
	 * <!--<script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.1.1/aes.js"></script>-->
	 * <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
	 * <script >
	 *
	 * const key = CryptoJS.enc.Utf8.parse("996a48e989a11fc4");
	 * const aes = {
	 * 	iv: CryptoJS.enc.Utf8.parse("685a446fb5ea77c2"),
	 * 	mode: CryptoJS.mode.CBC,
	 * 	padding: CryptoJS.pad.Pkcs7
	 * };
	 *
	 * function encrypt(data) {
	 * 	const encrypt = CryptoJS.AES.encrypt(data, key, aes);
	 * 	return encrypt.toString() // 已经过Base64编码
	 * }
	 * function decrypt(data) {
	 * 	const decrypt = CryptoJS.AES.decrypt(data, key, aes);
	 * 	return decrypt.toString(CryptoJS.enc.Utf8)
	 * }
	 *
	 * let params = {
	 * 	username: "91330382MA2CNA151B",
	 * 	phone: "123456",
	 * 	account: "helloworld"
	 * }
	 * console.log(JSON.stringify(params))
	 *
	 *
	 * let aesstr = encrypt(JSON.stringify(params));
	 * console.log("加密:", aesstr)
	 * let datastr = decrypt(aesstr)
	 * console.log("解密:", datastr)
	 *
	 *
	 * console.log("调用接口")
	 * axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
	 * axios.post("http://192.168.0.126:10001/test/index", {
	 * 	data: encrypt(JSON.stringify(params))
	 * }).then(function (res) {
	 * 	console.log(res.data);
	 * 	console.log(decrypt(res.data.data))
	 * }).catch(function (error) {
	 * 	console.log(error)
	 * })
	 *
	 * </script>
	 */
	public static SecretKey generateKeyFromSpecialKeyStr(String key) {
		try {
			return new SecretKeySpec(key.getBytes(UTF8), ALGORITHM_AES);
		} catch (Throwable cause) {
			throw new RuntimeException(cause);
		}
	}

	/**
	 * 生成密钥
	 */
	public static SecretKey generateRandomKeyFromKeyGenerator(KeySizeEnum keySize) {
		try {
			KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM_AES);
			keyGenerator.init(keySize.getLength());
			return keyGenerator.generateKey();
		} catch (Throwable cause) {
			throw new RuntimeException(cause);
		}
	}

	/**
	 * 生成密钥
	 */
	private static SecretKey generateRandomKeyFromPasswordStr(KeySizeEnum keySize, String password) {
		try {
			byte[] salt = new byte[128]; // 最小salt长度应为128位即16byte
			new SecureRandom().nextBytes(salt);
			PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt, 1024, keySize.getLength());
			SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM_PBKDF2WithHmacSHA256);
			SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
			return new SecretKeySpec(secretKey.getEncoded(), ALGORITHM_AES);
		} catch (Throwable cause) {
			throw new RuntimeException(cause);
		}
	}

	/**
	 * 生成密钥字符串(Base64编码)
	 */
	public static String generateKeyStrFromSpecialKeyStr(String key) {
		return toKeyStr(generateKeyFromSpecialKeyStr(key));
	}
	/**
	 * 生成密钥字符串(Base64编码)
	 */
	public static String generateRandomKeyStrFromKeyGenerator(KeySizeEnum keySize) {
		return toKeyStr(generateRandomKeyFromKeyGenerator(keySize));
	}

	/**
	 * 生成密钥字符串(Base64编码)
	 */
	public static String generateRandomKeyStringFromPasswordStr(KeySizeEnum keySize, String password) {
		return toKeyStr(generateRandomKeyFromPasswordStr(keySize, password));
	}

	/**
	 * 默认生成128位长度的密钥
	 */
	public static String generateKeyStr() {
		return generateRandomKeyStrFromKeyGenerator(KeySizeEnum.BIT_128);
	}

	/**
	 * 字符串密钥转标准密钥
	 */
	public static SecretKey toKey(String seyStr) {
		try {
			return new SecretKeySpec(decode(seyStr), ALGORITHM_AES);
		} catch (Throwable cause) {
			throw new RuntimeException(cause);
		}
	}

	/**
	 * 标准密钥转字符串密钥
	 */
	public static String toKeyStr(Key key) {
		return encode(key.getEncoded());
	}

	// 以下是生成向量相关方法 ---------- ---------- ---------- ---------- ----------

	/**
	 * 生成指定向量 (将指定字符串转换为向量)
	 * @param iv 为128位的字符串, 即占用16字节的字符串
	 */
	public static IvParameterSpec generateIvFromSpecialKeyStr(String iv) {
		try {
			return new IvParameterSpec(iv.getBytes(UTF8));
		} catch (Throwable cause) {
			throw new RuntimeException(cause);
		}
	}

	/**
	 * 生成指定字符串向量
	 */
	public static String generateIvStrFromSpecialKeyStr(String iv) {
		return toIVStr(generateIvFromSpecialKeyStr(iv));
	}

	/**
	 * 生成随机标准向量(IV与BlockSize一样, 都是128位16字节)
	 */
	public static IvParameterSpec generateRandomIv() {
		byte[] iv = new byte[16];
		new SecureRandom().nextBytes(iv);
		return new IvParameterSpec(iv);
	}

	/**
	 * 生成随机字符串向量
	 */
	public static String generateRandomIvStr() {
		return toIVStr(generateRandomIv());
	}

	/**
	 * 字符串向量转为标准向量
	 */
	public static IvParameterSpec toIV(String ivStr) {
		try {
			return new IvParameterSpec(decode(ivStr));
		} catch (Throwable cause) {
			throw new RuntimeException(cause);
		}
	}

	/**
	 * 标准向量转为字符串向量
	 */
	public static String toIVStr(IvParameterSpec iv) {
		return encode(iv.getIV());
	}

	// 以下是加密相关方法 ---------- ---------- ---------- ---------- ----------

	/**
	 * 加密
	 */
	public static byte[] encryptByKeyAndIv(SecretKey key, AlgorithmParameterSpec iv, byte[] data) {
		try {
			Cipher cipher = Cipher.getInstance(TRANSFORMATION);
			cipher.init(Cipher.ENCRYPT_MODE, key, iv);
			return cipher.doFinal(data);
		} catch (Throwable cause) {
			throw new RuntimeException(cause);
		}
	}

	/**
	 * 加密
	 */
	public static String encryptToStrByKeyStrAndIvStr(String keyStr, String ivStr, String data) {
		return encode(encryptByKeyAndIv(toKey(keyStr), toIV(ivStr), data.getBytes(UTF8)));
	}

	/**
	 * 加密(使用默认IV)
	 */
	public static byte[] encryptByKeyAndDefaultIv(SecretKey key, byte[] data) {
		return encryptByKeyAndIv(key, DEFAULT_IV, data);
	}

	/**
	 * 加密(使用默认IV)
	 */
	public static String encryptToStrByKeyStrAndDefaultIv(String keyStr, String data) {
		return encode(encryptByKeyAndIv(toKey(keyStr), DEFAULT_IV, data.getBytes(UTF8)));
	}

	// 以下是解密相关方法 ---------- ---------- ---------- ---------- ----------

	/**
	 * 解密
	 */
	public static byte[] decryptByKeyAndIv(SecretKey key, AlgorithmParameterSpec iv, byte[] data) {
		try {
			Cipher cipher = Cipher.getInstance(TRANSFORMATION);
			cipher.init(Cipher.DECRYPT_MODE, key, iv);
			return cipher.doFinal(data);
		} catch (Throwable cause) {
			throw new RuntimeException(cause);
		}
	}

	/**
	 * 解密
	 */
	public static String decryptToStrByKeyStrAndIvStr(String keyStr, String ivStr, String data) {
		return new String(decryptByKeyAndIv(toKey(keyStr), toIV(ivStr), decode(data)), UTF8);
	}

	/**
	 * 解密(使用默认IV)
	 */
	public static byte[] decryptByKeyAndDefaultIv(SecretKey key, byte[] data) {
		return decryptByKeyAndIv(key, DEFAULT_IV, data);
	}

	/**
	 * 解密(使用默认IV)
	 */
	public static String decryptToStrByKeyStrAndDefaultIv(String keyStr, String data) {
		return new String(decryptByKeyAndIv(toKey(keyStr), DEFAULT_IV, decode(data)), UTF8);
	}

	/**
	 * 编码
	 */
	private static String encode(byte[] data) {
		return Base64.getEncoder().encodeToString(data);
	}

	/**
	 * 解码
	 */
	private static byte[] decode(String data) {
		return Base64.getDecoder().decode(data);
	}

}
